Skip to content

循环引用处理的区别 ​

本节目标:理解“循环引用处理的区别”的核心思路,并能把它用于实际开发或面试表达。 太好了,你想深入具体细节,这些确实是 CommonJS 和 ESM 的核心区别。下面我分为三块分别说明:

一、Module Cache 模块缓存机制1. CommonJS:require.cache

  • require 加载模块后,会把它的 module.exports 缓存在 require.cache 中。
  • 第二次 require 同一个模块时,不再重新执行,只返回缓存的 exports。
  • 缓存的是模块执行后的结果。

流程图(简化): require('a') → 查 require.cache → 无 → 读取 a.js → 执行模块(包一层函数)→ module.exports 放入 require.cache → 返回 exports 2. ESM:Module Record 缓存

  • ESM 的缓存由 V8 实现,基于 Module Record。
  • 缓存的是一个包含所有 export 的模块命名空间对象,即 ModuleNamespaceObject。
  • 模块执行只发生一次,后续 import 都复用同一份命名空间对象。

二、循环引用处理(circular dependency)1. CommonJS 的循环引用

  • 模块在第一次执行还没完成前就被引用,module.exports 会是一个半成品。
  • 后续模块访问的是当前 exports 的初始状态,不是最终完整导出。

示例: // a.jsconst b = require('./b');console.log('a.js', b);module.exports = { fromA: 'A' }; // b.jsconst a = require('./a');console.log('b.js', a);module.exports = { fromB: 'B' }; 输出: b.js {}a.js { fromB: 'B' }

  • 说明:b.js 引用了 a.js,但 a.js 还没执行完,因此拿到的是空对象 {}。

2. ESM 的循环引用

  • 由于 ESM 是静态分析并建立 Module Graph(模块图),所以可以提前知道循环依赖。
  • ESM 在模块初始化阶段就设置好 export 的引用,即使模块尚未执行完,其他模块也能访问并实时更新(Live Binding)。

示例: // a.mjsimport { valueFromB } from './b.mjs';console.log('a.mjs', valueFromB);export const valueFromA = 'A'; // b.mjsimport { valueFromA } from './a.mjs';console.log('b.mjs', valueFromA);export const valueFromB = 'B'; 输出: b.mjs undefineda.mjs B 说明:虽然 b.mjs 访问了 a.mjs 的变量,但是实时绑定的,值是 undefined 而非空对象。

三、V8 中的 Module 实现机制1. V8 中的 Module Record(模块记录)

  • V8 对每个模块创建一个 Module Record 对象:
    • 包含模块路径、导入依赖、导出变量、命名空间对象等。
    • 每个 import 是一个引用,不是值拷贝。

2. 实际结构包含以下核心组成:

字段描述
[[ModuleNamespace]]代表该模块导出的命名空间对象
[[Status]]当前状态:uninstantiated → instantiating → instantiated → evaluated
[[Dependencies]]导入的其它模块
[[Environment]]执行环境,类似作用域

3. ESM 加载过程分为两阶段:① 实例化阶段(Instantiation Phase):

  • 扫描 import/export
  • 建立依赖图(Module Graph)
  • 为导出变量分配空间(live binding)

② 执行阶段(Evaluation Phase):

  • 按依赖顺序执行模块代码
  • 支持异步 top-level await

V8 如何处理循环依赖?

  • V8 在执行前先实例化所有模块,建立所有导出变量的引用(指针)。
  • 哪怕值还没赋好,其他模块访问时也会拿到“引用”,并随着初始化完成而更新。

总结:关键差异对照

特性CommonJSESM
缓存机制require.cacheV8 Module Record
缓存内容执行结果的 module.exports命名空间对象(含 live binding)
包装方式包装函数原生支持,无需包装
循环引用支持有限支持,引用时值未必完整完整支持,引用是 live binding
状态跟踪无显式状态由 V8 维护 Module 状态(状态机)
实现粒度脚本运行时实现编译+运行期分离,便于优化和静态分析

如果你对 Node.js 的源码(如 module.js)、V8 的 SourceTextModule 实现或想了解如何模拟一个 module loader(自己写 require/import 系统)我也可以进一步展开。是否想看某段具体源码或模拟实现? > 来自 <https://chatgpt.com/c/6805aef6-e7b4-8004-ab43-9a137f666985>

本站总访问 — 次